<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Schema DSL to JSON Converter</title>
  <style>
* {
  box-sizing: border-box;
}

body {
  font-family: Helvetica, Arial, sans-serif;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  line-height: 1.6;
  background-color: #f9f9f9;
  color: #333;
}

h1 {
  margin-bottom: 5px;
  color: #2c3e50;
}

.subtitle {
  color: #7f8c8d;
  margin-top: 0;
  margin-bottom: 20px;
}

.app-container {
  background-color: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.input-container {
  margin-bottom: 20px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #2c3e50;
}

textarea {
  width: 100%;
  min-height: 120px;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
  font-family: Helvetica, Arial, sans-serif;
  resize: vertical;
}

textarea:focus {
  border-color: #3498db;
  outline: none;
  box-shadow: 0 0 5px rgba(52, 152, 219, 0.5);
}

.output-container {
  background-color: #f7fafe;
  border-radius: 4px;
  overflow: hidden;
}

.output-header {
  display: flex;
  justify-content: space-between;
  background-color: #edf2f7;
  padding: 8px 15px;
  border-bottom: 1px solid #e2e8f0;
}

.output-title {
  font-weight: bold;
  color: #2c3e50;
  margin: 0;
}

.json-container {
  padding: 15px;
  overflow-x: auto;
  white-space: pre-wrap;
  font-family: monospace;
  color: #2c3e50;
  min-height: 100px;
}

.processing-indicator {
  color: #7f8c8d;
  font-style: italic;
}

.status {
  margin-top: 20px;
  font-style: italic;
  color: #666;
}

.controls {
  margin-bottom: 15px;
  display: flex;
  align-items: center;
}

.checkbox-container {
  display: flex;
  align-items: center;
  margin-right: 15px;
}

input[type="checkbox"] {
  margin-right: 5px;
}

.example-button {
  background-color: #3498db;
  color: white;
  border: none;
  padding: 8px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  margin-left: 10px;
}

.example-button:hover {
  background-color: #2980b9;
}

.loading {
  display: inline-block;
  margin-left: 10px;
  animation: spin 2s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.key {
  color: #e74c3c;
}

.string {
  color: #27ae60;
}

.number {
  color: #2980b9;
}

.boolean {
  color: #8e44ad;
}

.null {
  color: #7f8c8d;
}
  </style>
</head>
<body>
  <h1>Schema DSL to JSON converter</h1>
  <p class="subtitle">Type a compact schema definition and see it converted to JSON schema in real-time using llm.utils</p>
  
  <div class="app-container">
    <div class="controls">
      <div class="checkbox-container">
        <input type="checkbox" id="multiCheckbox">
        <label for="multiCheckbox">Array items schema</label>
      </div>
      <button id="exampleButton" class="example-button">Load example</button>
    </div>
    
    <div class="input-container">
      <label for="inputText">Enter schema DSL:</label>
      <textarea id="inputText" placeholder="name str: User's full name
age int: User's age in years
email str: Contact email address"></textarea>
    </div>
    
    <div class="output-container">
      <div class="output-header">
        <h3 class="output-title">JSON Schema</h3>
        <div id="processingTime"></div>
      </div>
      <div id="jsonOutput" class="json-container">Result will appear here as you type...</div>
    </div>
    
    <div id="status" class="status">Loading Python... <span class="loading">⟳</span></div>
  </div>

<script type="module">
// Constants for pyodide CDN URLs
const PYODIDE_URLS = [
  "https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js",
  "https://pyodide-cdn2.iodide.io/v0.24.1/full/pyodide.js",
  "https://cdn.skypack.dev/pyodide@0.24.1"
];

const statusElement = document.getElementById('status');
const inputTextarea = document.getElementById('inputText');
const jsonOutput = document.getElementById('jsonOutput');
const processingTime = document.getElementById('processingTime');
const multiCheckbox = document.getElementById('multiCheckbox');
const exampleButton = document.getElementById('exampleButton');

// Flag to track if we're using the imported llm.utils function or fallback
let usingImportedFunction = false;

// Keep track of loading attempts
let currentUrlIndex = 0;
let pyodide = null;
let debounceTimeout = null;
const DEBOUNCE_DELAY = 300; // ms

// Function to load pyodide from different CDNs
async function attemptLoadPyodide() {
  if (currentUrlIndex >= PYODIDE_URLS.length) {
    statusElement.innerHTML = 'Failed to load Python after multiple attempts. Please try refreshing the page.';
    return false;
  }
  
  const currentUrl = PYODIDE_URLS[currentUrlIndex];
  statusElement.innerHTML = `Loading Python from source ${currentUrlIndex + 1}... <span class="loading">⟳</span>`;
  
  try {
    // Clear any previous script tags
    document.querySelectorAll('script[data-pyodide-script]').forEach(script => script.remove());
    
    // Create and append the script tag
    const script = document.createElement('script');
    script.setAttribute('data-pyodide-script', 'true');
    script.src = currentUrl;
    script.async = true;
    
    // Create a promise that resolves when the script loads or rejects on error
    const scriptLoadPromise = new Promise((resolve, reject) => {
      script.onload = resolve;
      script.onerror = reject;
    });
    
    // Append the script to the document
    document.head.appendChild(script);
    
    // Wait for the script to load
    await scriptLoadPromise;
    
    // Once loaded, initialize pyodide
    if (window.loadPyodide) {
      pyodide = await window.loadPyodide();
      
      // Set up the Python environment with our schema_dsl function
      await initializePythonEnvironment();
      
      statusElement.textContent = 'Python loaded and ready! Start typing to see the JSON schema.';
      return true;
    } else {
      throw new Error('Pyodide loaded but loadPyodide function not found');
    }
  } catch (error) {
    console.error(`Failed to load pyodide from ${currentUrl}:`, error);
    currentUrlIndex++;
    return attemptLoadPyodide(); // Try the next URL
  }
}

// Alternative direct loading method if dynamic script loading fails
async function loadPyodideDirectly() {
  try {
    statusElement.innerHTML = 'Loading Python using direct import... <span class="loading">⟳</span>';
    
    for (const url of PYODIDE_URLS) {
      try {
        const pyodideModule = await import(url);
        if (pyodideModule && pyodideModule.loadPyodide) {
          pyodide = await pyodideModule.loadPyodide();
          
          // Set up the Python environment with our schema_dsl function
          await initializePythonEnvironment();
          
          statusElement.textContent = 'Python loaded and ready! Start typing to see the JSON schema.';
          return true;
        }
      } catch (importError) {
        console.warn(`Import failed for ${url}:`, importError);
        // Continue to the next URL
      }
    }
    
    throw new Error('All import attempts failed');
  } catch (error) {
    console.error('Failed all direct import attempts:', error);
    statusElement.textContent = 'Could not load Python. Using JavaScript fallback.';
    return false;
  }
}

// Initialize the Python environment
async function initializePythonEnvironment() {
    statusElement.innerHTML = 'Setting up environment <span class="loading">⟳</span>';
    await pyodide.runPythonAsync(`
from typing import Dict, Any

def schema_dsl(schema_dsl: str, multi: bool = False) -> Dict[str, Any]:
    """
    Build a JSON schema from a concise schema string.

    Args:
        schema_dsl: A string representing a schema in the concise format.
            Can be comma-separated or newline-separated.
        multi: Boolean, return a schema for an "items" array of these

    Returns:
        A dictionary representing the JSON schema.
    """
    # Type mapping dictionary
    type_mapping = {
        "int": "integer",
        "float": "number",
        "bool": "boolean",
        "str": "string",
    }

    # Initialize the schema dictionary with required elements
    json_schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}

    # Check if the schema is newline-separated or comma-separated
    if "\\n" in schema_dsl:
        fields = [field.strip() for field in schema_dsl.split("\\n") if field.strip()]
    else:
        fields = [field.strip() for field in schema_dsl.split(",") if field.strip()]

    # Process each field
    for field in fields:
        # Extract field name, type, and description
        if ":" in field:
            field_info, description = field.split(":", 1)
            description = description.strip()
        else:
            field_info = field
            description = ""

        # Process field name and type
        field_parts = field_info.strip().split()
        field_name = field_parts[0].strip()

        # Default type is string
        field_type = "string"

        # If type is specified, use it
        if len(field_parts) > 1:
            type_indicator = field_parts[1].strip()
            if type_indicator in type_mapping:
                field_type = type_mapping[type_indicator]

        # Add field to properties
        json_schema["properties"][field_name] = {"type": field_type}

        # Add description if provided
        if description:
            json_schema["properties"][field_name]["description"] = description

        # Add field to required list
        json_schema["required"].append(field_name)

    if multi:
        return multi_schema(json_schema)
    else:
        return json_schema


def multi_schema(schema: dict) -> dict:
    "Wrap JSON schema in an 'items': [] array"
    return {
        "type": "object",
        "properties": {"items": {"type": "array", "items": schema}},
        "required": ["items"],
    }
    `);
}

// Function to convert schema DSL to JSON Schema using Python
async function convertWithPython(schemaDSL, multi) {
  try {
    const startTime = performance.now();
    
    // Call the schema_dsl function from llm.utils (with fallback)
    const result = await pyodide.runPythonAsync(`
# Use the imported function, with fallback to our local implementation
try:
    from llm.utils import schema_dsl
except ImportError:
    pass
    
out = schema_dsl(${JSON.stringify(schemaDSL)}, ${multi ? 'True' : 'False'})
print(out)
out
    `);
    
    const endTime = performance.now();
    processingTime.textContent = usingImportedFunction 
      ? `Processed in ${(endTime - startTime).toFixed(2)}ms using llm.utils` 
      : `Processed in ${(endTime - startTime).toFixed(2)}ms`;
    
    // Convert the Python result to a JavaScript object
    return convertMapsToObjects(result.toJs());
  } catch (error) {
    console.error('Python conversion error:', error);
    return {
      error: true,
      message: `Python error: ${error.message}`,
    };
  }
}

/**
 * Converts an object containing Map instances into a plain JavaScript object
 * @param {Object} input - The input object which may contain Map instances
 * @return {Object} A plain JavaScript object with Maps converted to objects
 */
 function convertMapsToObjects(input) {
  // Handle null or undefined
  if (input === null || input === undefined) {
    return input;
  }

  // Handle Map objects
  if (input instanceof Map) {
    const result = {};
    input.forEach((value, key) => {
      // Convert key to string if it's not already (Map keys can be any type)
      const stringKey = typeof key === 'object' ? JSON.stringify(key) : String(key);
      // Recursively convert the value in case it also contains Maps
      result[stringKey] = convertMapsToObjects(value);
    });
    return result;
  }

  // Handle arrays
  if (Array.isArray(input)) {
    return input.map(item => convertMapsToObjects(item));
  }

  // Handle plain objects (but not other types like Date, RegExp, etc.)
  if (typeof input === 'object') {
    const result = {};
    for (const key in input) {
      if (Object.prototype.hasOwnProperty.call(input, key)) {
        result[key] = convertMapsToObjects(input[key]);
      }
    }
    return result;
  }

  // For primitive values (string, number, boolean)
  return input;
}


// Function to syntax highlight the JSON
function highlightJSON(json) {
  if (!json) return '';
  
  return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) {
    let cls = 'number';
    if (/^"/.test(match)) {
      if (/:$/.test(match)) {
        cls = 'key';
      } else {
        cls = 'string';
      }
    } else if (/true|false/.test(match)) {
      cls = 'boolean';
    } else if (/null/.test(match)) {
      cls = 'null';
    }
    return '<span class="' + cls + '">' + match + '</span>';
  });
}

// Debounced input handler
function handleInput() {
  if (debounceTimeout) {
    clearTimeout(debounceTimeout);
  }
  
  debounceTimeout = setTimeout(async () => {
    const schemaDSL = inputTextarea.value;
    const multi = multiCheckbox.checked;
    
    // Show processing indicator
    jsonOutput.innerHTML = '<div class="processing-indicator">Processing...</div>';
    
    // Convert the schema DSL
    const convertedSchema = await convertWithPython(schemaDSL, multi);
    
    jsonOutput.innerHTML = highlightJSON(JSON.stringify(convertedSchema, null, 2));
  }, DEBOUNCE_DELAY);
}

// Example button handler
function loadExample() {
  inputTextarea.value = `name str: User's full name
email str: User's email address for notifications
age int: User's age in years
is_active bool: Whether the user account is active
balance float: Current account balance in dollars`;
  
  handleInput();
}

// Start loading pyodide
async function initialize() {
  try {
    // First try the script loading approach
    const scriptLoadSuccess = await attemptLoadPyodide();
    
    // If that fails, try direct imports
    if (!scriptLoadSuccess) {
      const directSuccess = await loadPyodideDirectly();
      
      // If all loading methods fail, use JavaScript fallback
      if (!directSuccess) {
        statusElement.textContent = 'Python could not be loaded. Using JavaScript fallback instead.';
      }
    }
    
    // Add event listeners
    inputTextarea.addEventListener('input', handleInput);
    multiCheckbox.addEventListener('change', handleInput);
    exampleButton.addEventListener('click', loadExample);
    
    // Initial transformation
    handleInput();
  } catch (error) {
    console.error('Failed to initialize:', error);
    statusElement.textContent = 'Failed to initialize. Using JavaScript fallback.';
    
    // Add event listeners with JavaScript fallback
    inputTextarea.addEventListener('input', handleInput);
    multiCheckbox.addEventListener('change', handleInput);
    exampleButton.addEventListener('click', loadExample);
  }
}

// Initialize when the page loads
initialize();
</script>
</body>
</html>
